feat(lamzu): add the Atlantis generation under CompX vendor id 0x3554 - #85
Closed
bpavlina wants to merge 6 commits into
Closed
feat(lamzu): add the Atlantis generation under CompX vendor id 0x3554#85bpavlina wants to merge 6 commits into
bpavlina wants to merge 6 commits into
Conversation
The Atlantis family speaks CompX's report-8 interrupt protocol, not the feature-report page/command protocol the 0x373e and 0x37b0 Lamzu models use — the same protocol this package already implements for Pulsar's 4K receiver, so the framing, checksum, command ids and 50-step DPI encoding are imported from there rather than restated. Verified on a Lamzu Atlantis Mini 4K, firmware 1.24, wired 0x3554:0xf50f. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
- A late reply to a timed-out attempt could satisfy the next request: every flash access shares command 0x08/0x07, so a read of one address could return another's bytes and a setter could verify against the wrong field. Flash replies are now matched on the echoed address. - open() is memoized; two concurrent reads attached two listeners, of which close() removed one. - setDpiStageCount shrinks the cached stage list with the device. - setProfile validates against the four profiles the mouse accepts (0-3 write and read back; 4 and above are rejected with status 1). - A corrupt DPI stage read throws instead of reporting 50 DPI. - Dropped the redundant per-product picker filters: 0x3554 is already requested vendor-wide for the Pulsar 4K receiver. - Discovery tests, and the flash-field evidence table now says which fields the vendor UI confirmed and which were only round-tripped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
The Atlantis config collection carries only report 8, the request/reply channel, so startNotifications answers false and the app keeps polling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
isSupported gates on an output report 8 on the 0xff02/usage-2 collection, and Chrome's collection shape is not the platform's — so what navigator.hid actually reports is now in the notes rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
setDpiStageColor scaled an unchecked stage index straight into a flash address: stage -8 landed on the DPI stages at 12 and stage 13 on the button actions at 96, with the colour reading back cleanly from wherever it wrote. Every stage index is now bounded before it becomes an address. Also: - Setters hold the queue across write and read-back, so two concurrent calls for one field can no longer verify against each other's writes. - close() cancels: it settles the waiting exchange and stops queued work from reopening the device. - Switching profiles or changing the stage count drops the cached status, which described the old profile or the wrong stage list. - DPI stages decode per axis, so a stage Lamzu's app set to separate x and y no longer reads as corrupt. - The polling-rate family is declared per product instead of inferred from the rate ceiling, which had quietly decided it for the 1K receiver nobody has tested. - Timers come from globalThis, so the client is testable off-browser. - A fake HID device with real flash state now covers the request lifecycle, the setters, and each of the above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
The reentrancy flag it replaced could not tell a nested call from an unrelated one: a second caller arriving while an operation awaited a reply read the flag as nesting, ran inline, and overwrote the in-flight exchange's reply callback. Public methods now take the lock exactly once and work through unqueued helpers, so there is nothing to detect. Also: - open() and close() take turns. Overlapping them let a reopen memoize a resolved open while the close was still about to remove the listener, wedging every later open. - setProfile and setDpiStageCount drop the cache before the write rather than after the verification read, which could otherwise fail and leave a cache describing a device that had already changed. - dpiY follows dpi through a stage write and a stage switch instead of keeping the previous stage's value. - The concurrency and close tests now reach the failure modes they name: the second caller starts only once the device has a report in hand and its reply is withheld. Reintroducing the old flag fails the first; restoring it passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
Member
|
This PR conflicts with main (other driver additions landed in the meantime touching the shared registry.ts file). I've resolved the conflicts (pure additive merge, no logic changes — the Atlantis driver already correctly scopes itself to its own product IDs under the shared 0x3554 CompX vendor id) and opened #91 as a replacement, which is verified passing. Since I can't push to this fork branch, closing this one in favor of #91 — thank you for the Lamzu Atlantis work! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the Lamzu Atlantis family — Atlantis OG V2, Atlantis Mini, Atlantis Mini Pro, Thorn, Maya and Paro of that generation — as a driver of its own.
These are not the Lamzu the existing driver handles.
LamzuHidClientspeaks the CompX page/command protocol on feature report 0 under vendor ids0x373eand0x37b0. The Atlantis generation is a different CompX stack entirely: vendor id0x3554, report 8, interrupt reports, settings in a flash image addressed by byte offset.That stack is already in this repo — it is what
PulsarHidClientspeaks to the Pulsar 4K receiver. SamepulsarPacketChecksum, same command ids, same flash offsets, same 50-step DPI encoding. So this adds no new framing code:Why a separate driver rather than widening Pulsar
0x3554is CompX's ODM id, shared by four brands in this repo already. Today a Lamzu Atlantis falls throughPulsarHidClient's VGN branch, so it connects as a "Pulsar" device with a lift-off reading that is simply wrong. Adding the Lamzu ids toCLAIMED_VGN_PRODUCT_IDS— the mechanism already there for exactly this — hands them to a driver that knows what they are.flowchart TD D["HID device<br/>vid 0x3554"] --> C{product id} C -->|f520 f523 f5bb f522| T[Teevolution] C -->|fb56 fb57| V[VGN F2] C -->|f58e f58f| A[ATK / VXE] C -->|"f50f f50d f510 f517<br/><b>new</b>"| L[LamzuAtlantisHidClient] C -->|anything else| P[Pulsar 4K receiver] L --> S["brand: Lamzu · LOD 1 mm / 2 mm<br/>rate table with both 1000 Hz encodings"] P --> Q["brand: Pulsar · LOD Low/Med/High<br/>0x10 decoded as 2000 Hz"]Three things the wire does not do by the book
The battery reply lies about its length. Byte 4 says
0x02; four bytes follow. Decoding by the declared length silently drops the voltage, sopayloadis the full window anddeclaredLengthis reported separately.The percent byte is authoritative.
lamzu-cfgderives the percentage linearly from millivolts between 3,050 and 4,200. At 4,239 mV that reads 100%, where the mouse's own byte — and Lamzu's configurator on screen next to it — both said 95%.1,000 Hz has two encodings, as on the other Lamzu generations, so
pulsarDecodePollingRateis wrong here: it reads0x10as 2,000 Hz.0x020x010x100x200x400x80Reads decode both DPI axes
pulsarVgnDecodeDpireturns null unless a stage's two axis bytes are identical — the Pulsar driver only ever writes them in lockstep. A Lamzu configured with separate x and y stores a perfectly valid, correctly checksummed field that would read as corrupt, so reads use a Lamzu decoder that pulls each axis's high bits out of the flags byte (2-3 for x, 6-7 for y). Writes still go through the shared encoder, which flattens the axes; asymmetric writes aren't attempted without hardware to confirm them.Flash fields
sleepTime(173) is absent from lamzu-cfg's map. Found by diffing the flash image across a change in Lamzu's configurator —0x06at a 1-minute timeout,0x01at 10 seconds — so the byte counts ten-second units, not seconds.A field carries a trailing checksum, so value bytes plus that byte sum to
0x55:Identity
Six models share
0x3554:0xf50f, one USB product string (LAMZU Atlantis Pro) and one firmware version. Nothing on the wire separates them, and Lamzu's own configurator solves this by asking the user to pick the model from a list — so the catalog names the family,Lamzu Atlantis, rather than guessing a model.Hardware
Lamzu Atlantis Mini 4K, firmware 1.24 (
0x12→01 24, matching the USBbcdDeviceand Lamzu's download page), wired0x3554:0xf50f.0xf50fis markedverified: true. The three receiver ids come from Lamzu's device table and nothing more — no receiver has been exercised, so neither their rate lists nor the transport itself is established, and they stayverified: falseuntil one is. The rate family is stored per product rather than inferred from the rate ceiling, so the 1K receiver (which tops out at 1,000 Hz exactly like the cable) is not silently assigned an encoding.Not touched: button remapping, macros, pairing and factory reset. No command id outside the documented set was ever sent — the write path on this firmware includes a factory reset, so unknown ids were not probed.
Protocol groundwork: LeadSun/lamzu-cfg (Apache-2.0/MIT), reverse-engineered from an Atlantis Mini Pro. Full capture notes in
docs/lamzu-atlantis-testing.md.Companion app PR: OpenMouse-Project/openmouse#235
🤖 Generated with Claude Code